/*
 * Copyright (C)2011-2014 D. R. Commander.  All Rights Reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * - Redistributions of source code must retain the above copyright notice,
 *   this list of conditions and the following disclaimer.
 * - Redistributions in binary form must reproduce the above copyright notice,
 *   this list of conditions and the following disclaimer in the documentation
 *   and/or other materials provided with the distribution.
 * - Neither the name of the libjpeg-turbo Project nor the names of its
 *   contributors may be used to endorse or promote products derived from this
 *   software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS",
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

package org.libjpegturbo.turbojpeg;

import java.awt.image.*;
import java.nio.ByteOrder;

/**
 *
 * TurboJPEG decompressor.
 *
 * @since version 1.0,	01/05/2016
 *
 * @author Josh Aas, D.R. Commander, Dmitry Viktorov
 *
 */
public class TJDecompressor implements AutoCloseable {

    private static final String NO_ASSOC_ERROR = "No JPEG image is associated with this instance";

    protected long handle = 0;
    protected byte[] jpegBuf = null;
    protected int jpegBufSize = 0;
    protected YUVImage yuvImage = null;
    protected int jpegWidth = 0;
    protected int jpegHeight = 0;
    protected int jpegSubsamp = -1;
    protected int jpegColorspace = -1;
    private ByteOrder byteOrder = null;

    /**
     * Create a TurboJPEG decompresssor instance.
     */
    public TJDecompressor() throws Exception {
        init();
    }

    /**
     * Create a TurboJPEG decompressor instance and associate the JPEG source
     * image stored in <code>jpegImage</code> with the newly created instance.
     *
     * @param jpegImage JPEG image buffer (size of the JPEG image is assumed to
     *                  be the length of the array)
     */
    public TJDecompressor(byte[] jpegImage) throws Exception {
        init();
        setSourceImage(jpegImage, jpegImage.length);
    }

    /**
     * Create a TurboJPEG decompressor instance and associate the JPEG source
     * image of length <code>imageSize</code> bytes stored in
     * <code>jpegImage</code> with the newly created instance.
     *
     * @param jpegImage JPEG image buffer
     * @param imageSize size of the JPEG image (in bytes)
     */
    public TJDecompressor(byte[] jpegImage, int imageSize) throws Exception {
        init();
        setSourceImage(jpegImage, imageSize);
    }

    /**
     * Create a TurboJPEG decompressor instance and associate the YUV planar
     * source image stored in <code>yuvImage</code> with the newly created
     * instance.
     *
     * @param yuvImage {@link YUVImage} instance containing a YUV planar
     *                 image to be decoded
     */
    public TJDecompressor(YUVImage yuvImage) throws Exception {
        init();
        setSourceImage(yuvImage);
    }

    /**
     * Associate the JPEG image of length <code>imageSize</code> bytes stored in
     * <code>jpegImage</code> with this decompressor instance.  This image will
     * be used as the source image for subsequent decompress operations.
     *
     * @param jpegImage JPEG image buffer
     * @param imageSize size of the JPEG image (in bytes)
     */
    public void setSourceImage(byte[] jpegImage, int imageSize)
            throws Exception {
        if (jpegImage == null || imageSize < 1)
            throw new Exception("Invalid argument in setSourceImage()");
        jpegBuf = jpegImage;
        jpegBufSize = imageSize;
        decompressHeader(jpegBuf, jpegBufSize);
        yuvImage = null;
    }

    /**
     * @deprecated Use {@link #setSourceImage(byte[], int)} instead.
     */
    @Deprecated
    public void setJPEGImage(byte[] jpegImage, int imageSize) throws Exception {
        setSourceImage(jpegImage, imageSize);
    }

    /**
     * Associate the specified YUV planar source image with this decompressor
     * instance.  Subsequent decompress operations will decode this image into an
     * RGB or grayscale destination image.
     *
     * @param srcImage {@link YUVImage} instance containing a YUV planar image to
     *                 be decoded
     */
    public void setSourceImage(YUVImage srcImage) throws Exception {
        if (srcImage == null)
            throw new Exception("Invalid argument in setSourceImage()");
        yuvImage = srcImage;
        jpegBuf = null;
        jpegBufSize = 0;
    }


    /**
     * Returns the width of the source image (JPEG or YUV) associated with this
     * decompressor instance.
     *
     * @return the width of the source image (JPEG or YUV) associated with this
     * decompressor instance.
     */
    public int getWidth() throws Exception {
        if (yuvImage != null) {
            return yuvImage.getWidth();
        }
        if (jpegWidth < 1) {
            throw new Exception(NO_ASSOC_ERROR);
        }
        return jpegWidth;
    }

    /**
     * Returns the height of the source image (JPEG or YUV) associated with this
     * decompressor instance.
     *
     * @return the height of the source image (JPEG or YUV) associated with this
     * decompressor instance.
     */
    public int getHeight() throws Exception {
        if (yuvImage != null)
            return yuvImage.getHeight();
        if (jpegHeight < 1)
            throw new Exception(NO_ASSOC_ERROR);
        return jpegHeight;
    }

    /**
     * Returns the level of chrominance subsampling used in the source image
     * (JPEG or YUV) associated with this decompressor instance.  See
     * {@link TJ#SAMP_444 TJ.SAMP_*}.
     *
     * @return the level of chrominance subsampling used in the source image
     * (JPEG or YUV) associated with this decompressor instance.
     */
    public int getSubsamp() throws Exception {
        if (yuvImage != null) {
            return yuvImage.getSubsamp();
        }
        if (jpegSubsamp < 0) {
            throw new Exception(NO_ASSOC_ERROR);
        }
        if (jpegSubsamp >= TJ.NUMSAMP) {
            throw new Exception("JPEG header information is invalid");
        }
        return jpegSubsamp;
    }

    /**
     * Returns the colorspace used in the source image (JPEG or YUV) associated
     * with this decompressor instance.  See {@link TJ#CS_RGB TJ.CS_*}.  If the
     * source image is YUV, then this always returns {@link TJ#CS_YCbCr}.
     *
     * @return the colorspace used in the source image (JPEG or YUV) associated
     * with this decompressor instance.
     */
    public int getColorspace() throws Exception {
        if (yuvImage != null)
            return TJ.CS_YCbCr;
        if (jpegColorspace < 0)
            throw new Exception(NO_ASSOC_ERROR);
        if (jpegColorspace >= TJ.NUMCS)
            throw new Exception("JPEG header information is invalid");
        return jpegColorspace;
    }

    /**
     * Returns the JPEG image buffer associated with this decompressor instance.
     *
     * @return the JPEG image buffer associated with this decompressor instance.
     */
    public byte[] getJPEGBuf() throws Exception {
        if (jpegBuf == null) {
            throw new Exception(NO_ASSOC_ERROR);
        }
        return jpegBuf;
    }

    /**
     * Returns the size of the JPEG image (in bytes) associated with this
     * decompressor instance.
     *
     * @return the size of the JPEG image (in bytes) associated with this
     * decompressor instance.
     */
    public int getJPEGSize() throws Exception {
        if (jpegBufSize < 1) {
            throw new Exception(NO_ASSOC_ERROR);
        }
        return jpegBufSize;
    }

    /**
     * Returns the width of the largest scaled-down image that the TurboJPEG
     * decompressor can generate without exceeding the desired image width and
     * height.
     *
     * @param desiredWidth  desired width (in pixels) of the decompressed image.
     *                      Setting this to 0 is the same as setting it to the width of the JPEG image
     *                      (in other words, the width will not be considered when determining the
     *                      scaled image size.)
     * @param desiredHeight desired height (in pixels) of the decompressed image.
     *                      Setting this to 0 is the same as setting it to the height of the JPEG
     *                      image (in other words, the height will not be considered when determining
     *                      the scaled image size.)
     * @return the width of the largest scaled-down image that the TurboJPEG
     * decompressor can generate without exceeding the desired image width and
     * height.
     */
    public int getScaledWidth(int desiredWidth, int desiredHeight)
            throws Exception {
        if (jpegWidth < 1 || jpegHeight < 1)
            throw new Exception(NO_ASSOC_ERROR);
        if (desiredWidth < 0 || desiredHeight < 0)
            throw new Exception("Invalid argument in getScaledWidth()");
        TJScalingFactor[] sf = TJ.getScalingFactors();
        if (desiredWidth == 0)
            desiredWidth = jpegWidth;
        if (desiredHeight == 0)
            desiredHeight = jpegHeight;
        int scaledWidth = jpegWidth, scaledHeight = jpegHeight;
        for (int i = 0; i < sf.length; i++) {
            scaledWidth = sf[i].getScaled(jpegWidth);
            scaledHeight = sf[i].getScaled(jpegHeight);
            if (scaledWidth <= desiredWidth && scaledHeight <= desiredHeight)
                break;
        }
        if (scaledWidth > desiredWidth || scaledHeight > desiredHeight)
            throw new Exception("Could not scale down to desired image dimensions");
        return scaledWidth;
    }

    /**
     * Returns the height of the largest scaled-down image that the TurboJPEG
     * decompressor can generate without exceeding the desired image width and
     * height.
     *
     * @param desiredWidth  desired width (in pixels) of the decompressed image.
     *                      Setting this to 0 is the same as setting it to the width of the JPEG image
     *                      (in other words, the width will not be considered when determining the
     *                      scaled image size.)
     * @param desiredHeight desired height (in pixels) of the decompressed image.
     *                      Setting this to 0 is the same as setting it to the height of the JPEG
     *                      image (in other words, the height will not be considered when determining
     *                      the scaled image size.)
     * @return the height of the largest scaled-down image that the TurboJPEG
     * decompressor can generate without exceeding the desired image width and
     * height.
     */
    public int getScaledHeight(int desiredWidth, int desiredHeight)
            throws Exception {
        if (jpegWidth < 1 || jpegHeight < 1)
            throw new Exception(NO_ASSOC_ERROR);
        if (desiredWidth < 0 || desiredHeight < 0)
            throw new Exception("Invalid argument in getScaledHeight()");
        TJScalingFactor[] sf = TJ.getScalingFactors();
        if (desiredWidth == 0)
            desiredWidth = jpegWidth;
        if (desiredHeight == 0)
            desiredHeight = jpegHeight;
        int scaledWidth = jpegWidth, scaledHeight = jpegHeight;
        for (int i = 0; i < sf.length; i++) {
            scaledWidth = sf[i].getScaled(jpegWidth);
            scaledHeight = sf[i].getScaled(jpegHeight);
            if (scaledWidth <= desiredWidth && scaledHeight <= desiredHeight)
                break;
        }
        if (scaledWidth > desiredWidth || scaledHeight > desiredHeight)
            throw new Exception("Could not scale down to desired image dimensions");
        return scaledHeight;
    }

    /**
     * Decompress the JPEG source image or decode the YUV source image associated
     * with this decompressor instance and output a grayscale, RGB, or CMYK image
     * to the given destination buffer.
     *
     * @param dstBuf        buffer that will receive the decompressed/decoded image.
     *                      If the source image is a JPEG image, then this buffer should normally be
     *                      <code>pitch * scaledHeight</code> bytes in size, where
     *                      <code>scaledHeight</code> can be determined by calling <code>
     *                      scalingFactor.{@link TJScalingFactor#getScaled getScaled}(jpegHeight)
     *                      </code> with one of the scaling factors returned from {@link
     *                      TJ#getScalingFactors} or by calling {@link #getScaledHeight}.  If the
     *                      source image is a YUV image, then this buffer should normally be
     *                      <code>pitch * height</code> bytes in size, where <code>height</code> is
     *                      the height of the YUV image.  However, the buffer may also be larger than
     *                      the dimensions of the source image, in which case the <code>x</code>,
     *                      <code>y</code>, and <code>pitch</code> parameters can be used to specify
     *                      the region into which the source image should be decompressed/decoded.
     * @param x             x offset (in pixels) of the region in the destination image into
     *                      which the source image should be decompressed/decoded
     * @param y             y offset (in pixels) of the region in the destination image into
     *                      which the source image should be decompressed/decoded
     * @param desiredWidth  If the source image is a JPEG image, then this
     *                      specifies the desired width (in pixels) of the decompressed image (or
     *                      image region.)  If the desired destination image dimensions are different
     *                      than the source image dimensions, then TurboJPEG will use scaling in the
     *                      JPEG decompressor to generate the largest possible image that will fit
     *                      within the desired dimensions.  Setting this to 0 is the same as setting
     *                      it to the width of the JPEG image (in other words, the width will not be
     *                      considered when determining the scaled image size.)  This parameter is
     *                      ignored if the source image is a YUV image.
     * @param pitch         bytes per line of the destination image.  Normally, this
     *                      should be set to <code>scaledWidth * TJ.pixelSize(pixelFormat)</code> if
     *                      the destination image is unpadded, but you can use this to, for instance,
     *                      pad each line of the destination image to a 4-byte boundary or to
     *                      decompress/decode the source image into a region of a larger image.  NOTE:
     *                      if the source image is a JPEG image, then <code>scaledWidth</code> can be
     *                      determined by calling <code>
     *                      scalingFactor.{@link TJScalingFactor#getScaled getScaled}(jpegWidth)
     *                      </code> or by calling {@link #getScaledWidth}.  If the source image is a
     *                      YUV image, then <code>scaledWidth</code> is the width of the YUV image.
     *                      Setting this parameter to 0 is the equivalent of setting it to
     *                      <code>scaledWidth * TJ.pixelSize(pixelFormat)</code>.
     * @param desiredHeight If the source image is a JPEG image, then this
     *                      specifies the desired height (in pixels) of the decompressed image (or
     *                      image region.)  If the desired destination image dimensions are different
     *                      than the source image dimensions, then TurboJPEG will use scaling in the
     *                      JPEG decompressor to generate the largest possible image that will fit
     *                      within the desired dimensions.  Setting this to 0 is the same as setting
     *                      it to the height of the JPEG image (in other words, the height will not be
     *                      considered when determining the scaled image size.)  This parameter is
     *                      ignored if the source image is a YUV image.
     * @param pixelFormat   pixel format of the decompressed/decoded image (one of
     *                      {@link TJ#PF_RGB TJ.PF_*})
     * @param flags         the bitwise OR of one or more of
     *                      {@link TJ#FLAG_BOTTOMUP TJ.FLAG_*}
     */
    public void decompress(byte[] dstBuf, int x, int y, int desiredWidth,
                           int pitch, int desiredHeight, int pixelFormat,
                           int flags) throws Exception {
        if (jpegBuf == null && yuvImage == null)
            throw new Exception(NO_ASSOC_ERROR);
        if (dstBuf == null || x < 0 || y < 0 || pitch < 0 ||
                (yuvImage != null && (desiredWidth < 0 || desiredHeight < 0)) ||
                pixelFormat < 0 || pixelFormat >= TJ.NUMPF || flags < 0)
            throw new Exception("Invalid argument in decompress()");
        if (yuvImage != null)
            decodeYUV(yuvImage.getPlanes(), yuvImage.getOffsets(),
                    yuvImage.getStrides(), yuvImage.getSubsamp(), dstBuf, x, y,
                    yuvImage.getWidth(), pitch, yuvImage.getHeight(), pixelFormat,
                    flags);
        else {
            if (x > 0 || y > 0)
                decompress(jpegBuf, jpegBufSize, dstBuf, x, y, desiredWidth, pitch,
                        desiredHeight, pixelFormat, flags);
            else
                decompress(jpegBuf, jpegBufSize, dstBuf, desiredWidth, pitch,
                        desiredHeight, pixelFormat, flags);
        }
    }

    /**
     * @deprecated Use
     * {@link #decompress(byte[], int, int, int, int, int, int, int)} instead.
     */
    @Deprecated
    public void decompress(byte[] dstBuf, int desiredWidth, int pitch,
                           int desiredHeight, int pixelFormat, int flags)
            throws Exception {
        decompress(dstBuf, 0, 0, desiredWidth, pitch, desiredHeight, pixelFormat,
                flags);
    }

    /**
     * Decompress the JPEG source image associated with this decompressor
     * instance and return a buffer containing the decompressed image.
     *
     * @param desiredWidth  see
     *                      {@link #decompress(byte[], int, int, int, int, int, int, int)}
     *                      for description
     * @param pitch         see
     *                      {@link #decompress(byte[], int, int, int, int, int, int, int)}
     *                      for description
     * @param desiredHeight see
     *                      {@link #decompress(byte[], int, int, int, int, int, int, int)}
     *                      for description
     * @param pixelFormat   pixel format of the decompressed image (one of
     *                      {@link TJ#PF_RGB TJ.PF_*})
     * @param flags         the bitwise OR of one or more of
     *                      {@link TJ#FLAG_BOTTOMUP TJ.FLAG_*}
     * @return a buffer containing the decompressed image.
     */
    public byte[] decompress(int desiredWidth, int pitch, int desiredHeight,
                             int pixelFormat, int flags) throws Exception {
        if (pitch < 0 ||
                (yuvImage == null && (desiredWidth < 0 || desiredHeight < 0)) ||
                pixelFormat < 0 || pixelFormat >= TJ.NUMPF || flags < 0)
            throw new Exception("Invalid argument in decompress()");
        int pixelSize = TJ.getPixelSize(pixelFormat);
        int scaledWidth = getScaledWidth(desiredWidth, desiredHeight);
        int scaledHeight = getScaledHeight(desiredWidth, desiredHeight);
        if (pitch == 0)
            pitch = scaledWidth * pixelSize;
        byte[] buf = new byte[pitch * scaledHeight];
        decompress(buf, desiredWidth, pitch, desiredHeight, pixelFormat, flags);
        return buf;
    }

    /**
     * Decompress the JPEG source image associated with this decompressor
     * instance into a YUV planar image and store it in the given
     * <code>YUVImage</code> instance.  This method performs JPEG decompression
     * but leaves out the color conversion step, so a planar YUV image is
     * generated instead of an RGB or grayscale image.  This method cannot be
     * used to decompress JPEG source images with the CMYK or YCCK colorspace.
     *
     * @param dstImage {@link YUVImage} instance that will receive the YUV planar
     *                 image.  The level of subsampling specified in this <code>YUVImage</code>
     *                 instance must match that of the JPEG image, and the width and height
     *                 specified in the <code>YUVImage</code> instance must match one of the
     *                 scaled image sizes that TurboJPEG is capable of generating from the JPEG
     *                 source image.
     * @param flags    the bitwise OR of one or more of
     *                 {@link TJ#FLAG_BOTTOMUP TJ.FLAG_*}
     */
    public void decompressToYUV(YUVImage dstImage, int flags) throws Exception {
        if (jpegBuf == null)
            throw new Exception(NO_ASSOC_ERROR);
        if (dstImage == null || flags < 0)
            throw new Exception("Invalid argument in decompressToYUV()");
        int scaledWidth = getScaledWidth(dstImage.getWidth(),
                dstImage.getHeight());
        int scaledHeight = getScaledHeight(dstImage.getWidth(),
                dstImage.getHeight());
        if (scaledWidth != dstImage.getWidth() ||
                scaledHeight != dstImage.getHeight())
            throw new Exception("YUVImage dimensions do not match one of the scaled image sizes that TurboJPEG is capable of generating.");
        if (jpegSubsamp != dstImage.getSubsamp())
            throw new Exception("YUVImage subsampling level does not match that of the JPEG image");

        decompressToYUV(jpegBuf, jpegBufSize, dstImage.getPlanes(),
                dstImage.getOffsets(), dstImage.getWidth(),
                dstImage.getStrides(), dstImage.getHeight(), flags);
    }

    /**
     * @deprecated Use {@link #decompressToYUV(YUVImage, int)} instead.
     */
    @Deprecated
    public void decompressToYUV(byte[] dstBuf, int flags) throws Exception {
        YUVImage dstImage = new YUVImage(dstBuf, jpegWidth, 4, jpegHeight,
                jpegSubsamp);
        decompressToYUV(dstImage, flags);
    }

    /**
     * Decompress the JPEG source image associated with this decompressor
     * instance into a set of Y, U (Cb), and V (Cr) image planes and return a
     * <code>YUVImage</code> instance containing the decompressed image planes.
     * This method performs JPEG decompression but leaves out the color
     * conversion step, so a planar YUV image is generated instead of an RGB or
     * grayscale image.  This method cannot be used to decompress JPEG source
     * images with the CMYK or YCCK colorspace.
     *
     * @param desiredWidth  desired width (in pixels) of the YUV image.  If the
     *                      desired image dimensions are different than the dimensions of the JPEG
     *                      image being decompressed, then TurboJPEG will use scaling in the JPEG
     *                      decompressor to generate the largest possible image that will fit within
     *                      the desired dimensions.  Setting this to 0 is the same as setting it to
     *                      the width of the JPEG image (in other words, the width will not be
     *                      considered when determining the scaled image size.)
     * @param strides       an array of integers, each specifying the number of bytes
     *                      per line in the corresponding plane of the output image.  Setting the
     *                      stride for any plane to 0 is the same as setting it to the scaled
     *                      component width of the plane.  If <tt>strides</tt> is NULL, then the
     *                      strides for all planes will be set to their respective scaled component
     *                      widths.  You can adjust the strides in order to add an arbitrary amount of
     *                      line padding to each plane.
     * @param desiredHeight desired height (in pixels) of the YUV image.  If the
     *                      desired image dimensions are different than the dimensions of the JPEG
     *                      image being decompressed, then TurboJPEG will use scaling in the JPEG
     *                      decompressor to generate the largest possible image that will fit within
     *                      the desired dimensions.  Setting this to 0 is the same as setting it to
     *                      the height of the JPEG image (in other words, the height will not be
     *                      considered when determining the scaled image size.)
     * @param flags         the bitwise OR of one or more of
     *                      {@link TJ#FLAG_BOTTOMUP TJ.FLAG_*}
     * @return a YUV planar image.
     */
    public YUVImage decompressToYUV(int desiredWidth, int[] strides,
                                    int desiredHeight,
                                    int flags) throws Exception {
        if (flags < 0)
            throw new Exception("Invalid argument in decompressToYUV()");
        if (jpegWidth < 1 || jpegHeight < 1 || jpegSubsamp < 0)
            throw new Exception(NO_ASSOC_ERROR);
        if (jpegSubsamp >= TJ.NUMSAMP)
            throw new Exception("JPEG header information is invalid");
        if (yuvImage != null)
            throw new Exception("Source image is the wrong type");

        int scaledWidth = getScaledWidth(desiredWidth, desiredHeight);
        int scaledHeight = getScaledHeight(desiredWidth, desiredHeight);
        YUVImage yuvImage = new YUVImage(scaledWidth, null, scaledHeight,
                jpegSubsamp);
        decompressToYUV(yuvImage, flags);
        return yuvImage;
    }

    /**
     * Decompress the JPEG source image associated with this decompressor
     * instance into a unified YUV planar image buffer and return a
     * <code>YUVImage</code> instance containing the decompressed image.  This
     * method performs JPEG decompression but leaves out the color conversion
     * step, so a planar YUV image is generated instead of an RGB or grayscale
     * image.  This method cannot be used to decompress JPEG source images with
     * the CMYK or YCCK colorspace.
     *
     * @param desiredWidth  desired width (in pixels) of the YUV image.  If the
     *                      desired image dimensions are different than the dimensions of the JPEG
     *                      image being decompressed, then TurboJPEG will use scaling in the JPEG
     *                      decompressor to generate the largest possible image that will fit within
     *                      the desired dimensions.  Setting this to 0 is the same as setting it to
     *                      the width of the JPEG image (in other words, the width will not be
     *                      considered when determining the scaled image size.)
     * @param pad           the width of each line in each plane of the YUV image will be
     *                      padded to the nearest multiple of this number of bytes (must be a power of
     *                      2.)
     * @param desiredHeight desired height (in pixels) of the YUV image.  If the
     *                      desired image dimensions are different than the dimensions of the JPEG
     *                      image being decompressed, then TurboJPEG will use scaling in the JPEG
     *                      decompressor to generate the largest possible image that will fit within
     *                      the desired dimensions.  Setting this to 0 is the same as setting it to
     *                      the height of the JPEG image (in other words, the height will not be
     *                      considered when determining the scaled image size.)
     * @param flags         the bitwise OR of one or more of
     *                      {@link TJ#FLAG_BOTTOMUP TJ.FLAG_*}
     * @return a YUV planar image.
     */
    public YUVImage decompressToYUV(int desiredWidth, int pad, int desiredHeight,
                                    int flags) throws Exception {
        if (flags < 0)
            throw new Exception("Invalid argument in decompressToYUV()");
        if (jpegWidth < 1 || jpegHeight < 1 || jpegSubsamp < 0)
            throw new Exception(NO_ASSOC_ERROR);
        if (jpegSubsamp >= TJ.NUMSAMP)
            throw new Exception("JPEG header information is invalid");
        if (yuvImage != null)
            throw new Exception("Source image is the wrong type");

        int scaledWidth = getScaledWidth(desiredWidth, desiredHeight);
        int scaledHeight = getScaledHeight(desiredWidth, desiredHeight);
        YUVImage yuvImage = new YUVImage(scaledWidth, pad, scaledHeight,
                jpegSubsamp);
        decompressToYUV(yuvImage, flags);
        return yuvImage;
    }

    /**
     * @deprecated Use {@link #decompressToYUV(int, int, int, int)} instead.
     */
    @Deprecated
    public byte[] decompressToYUV(int flags) throws Exception {
        YUVImage dstImage = new YUVImage(jpegWidth, 4, jpegHeight, jpegSubsamp);
        decompressToYUV(dstImage, flags);
        return dstImage.getBuf();
    }

    /**
     * Decompress the JPEG source image or decode the YUV source image associated
     * with this decompressor instance and output a grayscale, RGB, or CMYK image
     * to the given destination buffer.
     *
     * @param dstBuf        buffer that will receive the decompressed/decoded image.
     *                      If the source image is a JPEG image, then this buffer should normally be
     *                      <code>stride * scaledHeight</code> pixels in size, where
     *                      <code>scaledHeight</code> can be determined by calling <code>
     *                      scalingFactor.{@link TJScalingFactor#getScaled getScaled}(jpegHeight)
     *                      </code> with one of the scaling factors returned from {@link
     *                      TJ#getScalingFactors} or by calling {@link #getScaledHeight}.  If the
     *                      source image is a YUV image, then this buffer should normally be
     *                      <code>stride * height</code> pixels in size, where <code>height</code> is
     *                      the height of the YUV image.  However, the buffer may also be larger than
     *                      the dimensions of the JPEG image, in which case the <code>x</code>,
     *                      <code>y</code>, and <code>stride</code> parameters can be used to specify
     *                      the region into which the source image should be decompressed.
     * @param x             x offset (in pixels) of the region in the destination image into
     *                      which the source image should be decompressed/decoded
     * @param y             y offset (in pixels) of the region in the destination image into
     *                      which the source image should be decompressed/decoded
     * @param desiredWidth  If the source image is a JPEG image, then this
     *                      specifies the desired width (in pixels) of the decompressed image (or
     *                      image region.)  If the desired destination image dimensions are different
     *                      than the source image dimensions, then TurboJPEG will use scaling in the
     *                      JPEG decompressor to generate the largest possible image that will fit
     *                      within the desired dimensions.  Setting this to 0 is the same as setting
     *                      it to the width of the JPEG image (in other words, the width will not be
     *                      considered when determining the scaled image size.)  This parameter is
     *                      ignored if the source image is a YUV image.
     * @param stride        pixels per line of the destination image.  Normally, this
     *                      should be set to <code>scaledWidth</code>, but you can use this to, for
     *                      instance, decompress the JPEG image into a region of a larger image.
     *                      NOTE: if the source image is a JPEG image, then <code>scaledWidth</code>
     *                      can be determined by calling <code>
     *                      scalingFactor.{@link TJScalingFactor#getScaled getScaled}(jpegWidth)
     *                      </code> or by calling {@link #getScaledWidth}.  If the source image is a
     *                      YUV image, then <code>scaledWidth</code> is the width of the YUV image.
     *                      Setting this parameter to 0 is the equivalent of setting it to
     *                      <code>scaledWidth</code>.
     * @param desiredHeight If the source image is a JPEG image, then this
     *                      specifies the desired height (in pixels) of the decompressed image (or
     *                      image region.)  If the desired destination image dimensions are different
     *                      than the source image dimensions, then TurboJPEG will use scaling in the
     *                      JPEG decompressor to generate the largest possible image that will fit
     *                      within the desired dimensions.  Setting this to 0 is the same as setting
     *                      it to the height of the JPEG image (in other words, the height will not be
     *                      considered when determining the scaled image size.)  This parameter is
     *                      ignored if the source image is a YUV image.
     * @param pixelFormat   pixel format of the decompressed image (one of
     *                      {@link TJ#PF_RGB TJ.PF_*})
     * @param flags         the bitwise OR of one or more of
     *                      {@link TJ#FLAG_BOTTOMUP TJ.FLAG_*}
     */
    public void decompress(int[] dstBuf, int x, int y, int desiredWidth,
                           int stride, int desiredHeight, int pixelFormat,
                           int flags) throws Exception {
        if (jpegBuf == null && yuvImage == null)
            throw new Exception(NO_ASSOC_ERROR);
        if (dstBuf == null || x < 0 || y < 0 || stride < 0 ||
                (yuvImage != null && (desiredWidth < 0 || desiredHeight < 0)) ||
                pixelFormat < 0 || pixelFormat >= TJ.NUMPF || flags < 0)
            throw new Exception("Invalid argument in decompress()");
        if (yuvImage != null)
            decodeYUV(yuvImage.getPlanes(), yuvImage.getOffsets(),
                    yuvImage.getStrides(), yuvImage.getSubsamp(), dstBuf, x, y,
                    yuvImage.getWidth(), stride, yuvImage.getHeight(), pixelFormat,
                    flags);
        else
            decompress(jpegBuf, jpegBufSize, dstBuf, x, y, desiredWidth, stride,
                    desiredHeight, pixelFormat, flags);
    }

    /**
     * Decompress the JPEG source image or decode the YUV source image associated
     * with this decompressor instance and output a decompressed/decoded image to
     * the given <code>BufferedImage</code> instance.
     *
     * @param dstImage a <code>BufferedImage</code> instance that will receive
     *                 the decompressed/decoded image.  If the source image is a JPEG image, then
     *                 the width and height of the <code>BufferedImage</code> instance must match
     *                 one of the scaled image sizes that TurboJPEG is capable of generating from
     *                 the JPEG image.  If the source image is a YUV image, then the width and
     *                 height of the <code>BufferedImage</code> instance must match the width and
     *                 height of the YUV image.
     * @param flags    the bitwise OR of one or more of
     *                 {@link TJ#FLAG_BOTTOMUP TJ.FLAG_*}
     */
    public void decompress(BufferedImage dstImage, int flags) throws Exception {
        if (dstImage == null || flags < 0)
            throw new Exception("Invalid argument in decompress()");
        int desiredWidth = dstImage.getWidth();
        int desiredHeight = dstImage.getHeight();
        int scaledWidth, scaledHeight;

        if (yuvImage != null) {
            if (desiredWidth != yuvImage.getWidth() ||
                    desiredHeight != yuvImage.getHeight())
                throw new Exception("BufferedImage dimensions do not match the dimensions of the source image.");
            scaledWidth = yuvImage.getWidth();
            scaledHeight = yuvImage.getHeight();
        } else {
            scaledWidth = getScaledWidth(desiredWidth, desiredHeight);
            scaledHeight = getScaledHeight(desiredWidth, desiredHeight);
            if (scaledWidth != desiredWidth || scaledHeight != desiredHeight)
                throw new Exception("BufferedImage dimensions do not match one of the scaled image sizes that TurboJPEG is capable of generating.");
        }
        int pixelFormat;
        boolean intPixels = false;
        if (byteOrder == null)
            byteOrder = ByteOrder.nativeOrder();
        switch (dstImage.getType()) {
            case BufferedImage.TYPE_3BYTE_BGR:
                pixelFormat = TJ.PF_BGR;
                break;
            case BufferedImage.TYPE_4BYTE_ABGR:
            case BufferedImage.TYPE_4BYTE_ABGR_PRE:
                pixelFormat = TJ.PF_XBGR;
                break;
            case BufferedImage.TYPE_BYTE_GRAY:
                pixelFormat = TJ.PF_GRAY;
                break;
            case BufferedImage.TYPE_INT_BGR:
                if (byteOrder == ByteOrder.BIG_ENDIAN)
                    pixelFormat = TJ.PF_XBGR;
                else
                    pixelFormat = TJ.PF_RGBX;
                intPixels = true;
                break;
            case BufferedImage.TYPE_INT_RGB:
                if (byteOrder == ByteOrder.BIG_ENDIAN)
                    pixelFormat = TJ.PF_XRGB;
                else
                    pixelFormat = TJ.PF_BGRX;
                intPixels = true;
                break;
            case BufferedImage.TYPE_INT_ARGB:
            case BufferedImage.TYPE_INT_ARGB_PRE:
                if (byteOrder == ByteOrder.BIG_ENDIAN)
                    pixelFormat = TJ.PF_ARGB;
                else
                    pixelFormat = TJ.PF_BGRA;
                intPixels = true;
                break;
            default:
                throw new Exception("Unsupported BufferedImage format");
        }
        WritableRaster wr = dstImage.getRaster();
        if (intPixels) {
            SinglePixelPackedSampleModel sm =
                    (SinglePixelPackedSampleModel) dstImage.getSampleModel();
            int stride = sm.getScanlineStride();
            DataBufferInt db = (DataBufferInt) wr.getDataBuffer();
            int[] buf = db.getData();
            if (yuvImage != null)
                decodeYUV(yuvImage.getPlanes(), yuvImage.getOffsets(),
                        yuvImage.getStrides(), yuvImage.getSubsamp(), buf, 0, 0,
                        yuvImage.getWidth(), stride, yuvImage.getHeight(),
                        pixelFormat, flags);
            else {
                if (jpegBuf == null)
                    throw new Exception(NO_ASSOC_ERROR);
                decompress(jpegBuf, jpegBufSize, buf, 0, 0, scaledWidth, stride,
                        scaledHeight, pixelFormat, flags);
            }
        } else {
            ComponentSampleModel sm =
                    (ComponentSampleModel) dstImage.getSampleModel();
            int pixelSize = sm.getPixelStride();
            if (pixelSize != TJ.getPixelSize(pixelFormat))
                throw new Exception("Inconsistency between pixel format and pixel size in BufferedImage");
            int pitch = sm.getScanlineStride();
            DataBufferByte db = (DataBufferByte) wr.getDataBuffer();
            byte[] buf = db.getData();
            decompress(buf, 0, 0, scaledWidth, pitch, scaledHeight, pixelFormat,
                    flags);
        }
    }

    /**
     * Decompress the JPEG source image or decode the YUV source image associated
     * with this decompressor instance and return a <code>BufferedImage</code>
     * instance containing the decompressed/decoded image.
     *
     * @param desiredWidth      see
     *                          {@link #decompress(byte[], int, int, int, int, int, int, int)} for
     *                          description
     * @param desiredHeight     see
     *                          {@link #decompress(byte[], int, int, int, int, int, int, int)} for
     *                          description
     * @param bufferedImageType the image type of the <code>BufferedImage</code>
     *                          instance that will be created (for instance,
     *                          <code>BufferedImage.TYPE_INT_RGB</code>)
     * @param flags             the bitwise OR of one or more of
     *                          {@link TJ#FLAG_BOTTOMUP TJ.FLAG_*}
     * @return a <code>BufferedImage</code> instance containing the
     * decompressed/decoded image.
     */
    public BufferedImage decompress(int desiredWidth, int desiredHeight, int bufferedImageType, int flags) throws Exception {
        if ((yuvImage == null && (desiredWidth < 0 || desiredHeight < 0)) || flags < 0) {
            throw new Exception("Invalid argument in decompress()");
        }
        int scaledWidth = getScaledWidth(desiredWidth, desiredHeight);
        int scaledHeight = getScaledHeight(desiredWidth, desiredHeight);
        BufferedImage img = new BufferedImage(scaledWidth, scaledHeight, bufferedImageType);
        decompress(img, flags);
        return img;
    }

    /**
     * Free the native structures associated with this decompressor instance.
     */
    public void close() throws Exception {
        if (handle != 0) {
            destroy();
        }
    }

    protected void finalize() throws Throwable {
        try {
            close();
        } catch (Exception e) {
        } finally {
            super.finalize();
        }
    }

    private native void init() throws Exception;

    private native void destroy() throws Exception;

    private native void decompressHeader(byte[] srcBuf, int size)
            throws Exception;

    private native void decompress(byte[] srcBuf, int size, byte[] dstBuf,
                                   int desiredWidth, int pitch, int desiredHeight, int pixelFormat, int flags)
            throws Exception; // deprecated

    private native void decompress(byte[] srcBuf, int size, byte[] dstBuf, int x,
                                   int y, int desiredWidth, int pitch, int desiredHeight, int pixelFormat,
                                   int flags) throws Exception;

    private native void decompress(byte[] srcBuf, int size, int[] dstBuf,
                                   int desiredWidth, int stride, int desiredHeight, int pixelFormat,
                                   int flags) throws Exception; // deprecated

    private native void decompress(byte[] srcBuf, int size, int[] dstBuf, int x,
                                   int y, int desiredWidth, int stride, int desiredHeight, int pixelFormat,
                                   int flags) throws Exception;

    private native void decompressToYUV(byte[] srcBuf, int size, byte[] dstBuf,
                                        int flags) throws Exception; // deprecated

    private native void decompressToYUV(byte[] srcBuf, int size,
                                        byte[][] dstPlanes, int[] dstOffsets, int desiredWidth, int[] dstStrides,
                                        int desiredheight, int flags) throws Exception;

    private native void decodeYUV(byte[][] srcPlanes, int[] srcOffsets,
                                  int[] srcStrides, int subsamp, byte[] dstBuf, int x, int y, int width,
                                  int pitch, int height, int pixelFormat, int flags) throws Exception;

    private native void decodeYUV(byte[][] srcPlanes, int[] srcOffsets,
                                  int[] srcStrides, int subsamp, int[] dstBuf, int x, int y, int width,
                                  int stride, int height, int pixelFormat, int flags) throws Exception;

    static {
        AdvancedTJLoader.load();
    }

};
